/*
 * Copyright (C) 2013 rdrrlabs gmail com,
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.rdrrlabs.example.timer2app.core;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

import com.rdrrlabs.example.annotations.NonNull;
import com.rdrrlabs.example.liblabs.utils.Utils;

import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookieStore;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.concurrent.TimeUnit;

//-----------------------------------------------

/**
 * Holds our helper to connect our GAE instance.
 * It keeps the context of the current user account to use, the current
 * app engine cookie and can do authenticated POSTs.
 */
public class GAEHelper {

    private static final String TAG = GAEHelper.class.getSimpleName();
    private static final boolean DEBUG = true;
    private static final boolean USE_DEV_SERVER = Utils.isEmulator();

    /** Pref to store GAE account name. Type: String. */
    private static final String PREFS_KEY_GAE_ACCOUNT = "gae_account";

    /** Base GAE url without a final / */
    public static final String sGAEBaseURL =
        USE_DEV_SERVER ?
            "http://10.0.2.2:8080" :
            "http://myserver.appspot.com"; // TODO change

    private static final String sGAEAuthCookie =
        USE_DEV_SERVER ? "dev_appserver_login" : "ACSID";

    private static final GAEHelper sInstance = new GAEHelper();
    private CookieManager mCookieMan;
    private final State mCurrentState = new State();


    public static GAEHelper getInstance() {
        return sInstance;
    }

    private GAEHelper() {
        // Setup our own cookie manager
        mCookieMan = new CookieManager();
        CookieHandler.setDefault(mCookieMan);
    }

    @NonNull
    public State getCurrentState() {
        return mCurrentState;
    }

    public CookieManager getCookieManager() {
        return mCookieMan;
    }

    /**
     * Initialization when main activity gets started.
     * If the account name isn't present, try to read it from prefs.
     * If there is no auth cookie, try to get one.
     *
     * @param activity The calling activity. Needed by the account manager to ask for
     *  user permission to access App Engine, if not already granted.
     */
    public void onActivityStartedCheck(Activity activity) {
        if (DEBUG) Log.d(TAG, String.format("onActivityStartedCheck: %s", mCurrentState));
        if (mCurrentState.mAccountName == null) {
            // Read initial name from prefs
            GlobalContext gc = GlobalContext.getContext();
            PrefsStorage ps = gc.getPrefsStorage();
            mCurrentState.mAccountName = ps.getString(PREFS_KEY_GAE_ACCOUNT, null);
            mCurrentState.setState(
                            mCurrentState.mAccountName == null ? State.NO_ACCOUNT : State.SUCCESS);
        }

        if (mCurrentState.mAccountName != null && mCurrentState.mGAECookie == null) {
            Account account = validateAccount(activity, mCurrentState);
            if (account != null) {
                getAuthToken(activity, account, mCurrentState);
            }
        }
    }

    /**
     * Validate that the account exists in the Account Manager.
     * If there is no account name, sets the state to no-account and returns null.
     * If found, it returns the {@link Account} and sets the state to success.
     * If not found, it returns null and sets the state to invalid-account.
     * <p/>
     * This should typically be followed by a call to {@link #getAuthToken}.
     *
     * @return The {@link Account} found matching the {@link State} account name,
     *  or null if not found.  */
    public Account validateAccount(Activity activity, State state) {
        if (DEBUG) Log.d(TAG, String.format("validateAccount: %s", state));
        state.setState(State.PENDING, "Validating account.");

        if (state.mAccountName == null || state.mAccountName.isEmpty()) {
            state.setState(State.NO_ACCOUNT);
            return null;
        }

        AccountManager am = AccountManager.get(activity);
        for (Account account : am.getAccountsByType("com.google")) {
            if (mCurrentState.mAccountName.equals(account.name)) {
                state.setState(State.SUCCESS);
                return account;
            }
        }

        state.setState(State.INVALID_ACCOUNT);
        return null;
    }

    public void getAuthToken(final Activity activity, Account account, final State state) {
        if (DEBUG) Log.d(TAG, String.format("getAuthToken: %s", state));
        state.setState(State.PENDING, "Authenticating...");

        AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
            // Runs from the main thread
            @Override
            public void run(AccountManagerFuture<Bundle> future) {
                try {
                    Bundle result = future.getResult(120, TimeUnit.SECONDS);

                    GetCookieTask t = new GetCookieTask(state);
                    t.execute(result.getString(AccountManager.KEY_AUTHTOKEN));

                } catch (OperationCanceledException e) {
                    Log.e(TAG, "getAuthToken", e);
                    state.setState(State.AUTH_TOKEN_FAILED,
                                   "Authentication cancelled.");
                } catch (AuthenticatorException e) {
                    Log.e(TAG, "getAuthToken", e);
                    state.setState(State.AUTH_TOKEN_FAILED,
                                   "Authentication failed.");
                } catch (IOException e) {
                    Log.e(TAG, "getAuthToken", e);
                    state.setState(State.AUTH_TOKEN_FAILED,
                                   "Network error.");
                }
            }
        };

        AccountManager am = AccountManager.get(activity);
        am.getAuthToken(
                account,
                "ah",                   // authTokenType,
                null,                   // options,
                activity,               // activity,
                callback ,
                null);                  // handler
    }

    private class GetCookieTask extends AsyncTask<String, Void, Void> {
        private final State mState;

        public GetCookieTask(State state) {
            mState = state;
        }

        @Override
        protected Void doInBackground(String... params) {
            String authToken = params[0];

            if (DEBUG) Log.d(TAG, String.format("getAuthCookie: %s", mState));
            mState.setState(State.PENDING, "Authenticating with server...");

            // Note: The _ah/login request must match the protocol we'll use later,
            // either http or https. Cookies are ACSID vs SACSID and aren't interchangeable.
            String urlStr = sGAEBaseURL + "/_ah/login?";
            if (USE_DEV_SERVER) {
                urlStr += "email=test@example.com&action=Login&";
            }
            urlStr += "continue=" + sGAEBaseURL + "/&auth=" + authToken;

            try {
                URL url = new URL(urlStr);

                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setConnectTimeout(120*1000);
                conn.setReadTimeout(5*1000);
                conn.setInstanceFollowRedirects(false);

                conn.connect();
                int status = conn.getResponseCode();

                // We expect a 302 redirect on success.
                if (status != 302) {
                    if (DEBUG) Log.d(TAG, "GetCookieTask invalid HTTP status " + status);
                    mState.setState(State.AUTH_COOKIE_FAILED);
                    return null;
                }

                CookieStore st = mCookieMan.getCookieStore();
                for (HttpCookie cookie : st.getCookies()) {
                    if (sGAEAuthCookie.equals(cookie.getName())) {
                        // Success, we found our cookie
                        mState.mGAECookie = (HttpCookie) cookie.clone();
                        mState.setState(State.SUCCESS);
                        return null;
                    }
                }

                // If we reach here, we didn't get the right cookie.
                if (DEBUG) Log.d(TAG, "GetCookieTask -- cookie not found");
                mState.setState(State.AUTH_COOKIE_FAILED, "Authentication not supported by server.");

            } catch (Throwable tr) {
                // Probably a network error
                if (DEBUG) Log.d(TAG, "GetCookieTask", tr);
                mState.setState(State.AUTH_COOKIE_FAILED, "Network error.");
            }

            return null;
        }
    }

    /**
     * A new account has been choosen, with its cookie to be used for this session.
     * Save the info in the app prefs.
     *
     * Notify bus listeners that the account name has changed, if it did.
     *
     * @param context An android context
     * @param accountName The account name.
     * @param cookie The new auth cookie.
     */
    public void useAccount(Context context, String accountName, HttpCookie cookie) {
        if (DEBUG) {
            Log.d(TAG, "useAccount: " + accountName + ", cookie: " + cookie.toString());
        }
        mCurrentState.mGAECookie = cookie;

        if ((accountName == null && mCurrentState.mAccountName != null) || !accountName.equals(mCurrentState.mAccountName)) {
            mCurrentState.mAccountName = accountName;

            GlobalContext gc = GlobalContext.getContext();
            PrefsStorage ps = gc.getPrefsStorage();

            ps.putString(PREFS_KEY_GAE_ACCOUNT, accountName);
            ps.beginWriteAsync(context);
        }

        mCurrentState.setState(State.SUCCESS);
    }

    private static void busSend(int code, State state) {
        GlobalContext.getContext().getBus().send(code, state);
    }

    public static class State {
        public static final int PENDING             = 0;
        public static final int NO_ACCOUNT          = 10;
        public static final int INVALID_ACCOUNT     = 11;
        public static final int AUTH_TOKEN_FAILED   = 12;
        public static final int AUTH_COOKIE_FAILED  = 13;
        public static final int SUCCESS             = 42;

        public int mState;
        public String mDetail;

        public String mAccountName;
        public HttpCookie mGAECookie;

        /** Change the state and send a bus notification if it has actually changed. */
        public void setState(int state) {
            this.setState(state, null);
        }

        /** Change the state and send a bus notification if it has actually changed. */
        public void setState(int state, String detail) {
            if (mState != state ||
                    (detail == null && mDetail != null) ||
                    (detail != null && !detail.equals(mDetail))) {
                mState = state;
                mDetail = detail;
                busSend(BusConstants.GAE_ACCOUNT_STATE_CHANGED, this);
            }
        }

        @Override
        public String toString() {
            return "<GAE.State [" + mState +
                        "] name: " + mAccountName +
                        ", cookie: " + mGAECookie +
                        ", detail: " + mDetail +
                        ">";
        }


    }
}


